#!/usr/bin/python3
# -*- encoding: utf-8 -*-
'''
@File        :B站滑动验证.py
@Time        :2020/08/26 10:19:07
@Author      :hejiang
@Software    :vsCode
'''
'''
B站已弃用验证码
仅供参考
'''

from PIL import Image
from time import sleep
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver import ActionChains
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
import random


class LoginBiliBili:
    def __init__(self, username, password):
        """
        初始化数据

        :param username: bilibili账号
        :param password: 密码
        """
        self.username = username
        self.password = password
        # 定义浏览器
        self.browser = webdriver.Chrome()
        # 定义显示等待
        self.wait = WebDriverWait(self.browser, 50)
        # bilibili登录url
        self.url = 'https://passport.bilibili.com/login'

    def open(self):
        """
        打开浏览器, 进入登陆界面
        输入用户名, 密码
        点击登陆

        :return: None
        """
        # 打开浏览器, 进入登陆界面
        self.browser.get(self.url)

        # 用户名输入框
        username_input_box = self.wait.until(
            EC.presence_of_element_located(
                (By.XPATH, '//*[@id="login-username"]')))
        # 清空用户名输入框
        username_input_box.clear()
        # 将自己的用户名输入到用户名输入框
        username_input_box.send_keys(self.username)

        # 密码输入框
        password_input_box = self.wait.until(
            EC.presence_of_element_located(
                (By.XPATH, '//*[@id="login-passwd"]')))
        # 清空密码输入框
        password_input_box.clear()
        # 将自己的密码输入到密码输入框
        password_input_box.send_keys(self.password)

        # 登录按钮
        login_button = self.wait.until(
            EC.presence_of_element_located(
                (By.XPATH, '//*[@id="geetest-wrap"]/ul/li[5]/a[1]')))
        # 点击登录
        login_button.click()
        # 休眠, 让验证码图片加载出来
        time.sleep(2)

    def get_geetest_image(self):
        """
        获取极验验证码图片
        :return: c_image(王者验证图) ic_image(有缺失的验证图)
        """
        """
        完整的验证图
        页面源码:
        <canvas class="geetest_canvas_fullbg geetest_fade geetest_absolute" 
        height="160" width="260" style="display: none;"></canvas>
        """
        # 执行js 拿到canvas画布里面的图片数据
        js = 'return document.getElementsByClassName("geetest_canvas_fullbg")[0].toDataURL("image/png");'
        # 图片数据
        complete_img_data = self.browser.execute_script(js)
        # base64 编码的图片信息
        complete_img_base64 = complete_img_data.split(',')[1]
        # 转成bytes类型
        complete_img = base64.b64decode(complete_img_base64)
        # 加载图片 return 回去对比
        c_image = Image.open(BytesIO(complete_img))
        # c_image.show()
        # 保存图片 (可不必保存)
        c_image.save('c_image.png')
        """
        有缺失的验证码图

        页面源码:
        <canvas class="geetest_canvas_bg geetest_absolute" height="160" width="260"></canvas>
        """
        # 执行js 拿到canvas画布里的图片数据
        js = 'return document.getElementsByClassName("geetest_canvas_bg")[0].toDataURL("image/png");'
        # 图片数据
        incomplete_img_data = self.browser.execute_script(js)
        # base64 编码的图片信息
        incomplete_img_base64 = incomplete_img_data.split(',')[1]
        # 转为bytes类型
        incomplete_img = base64.b64decode(incomplete_img_base64)
        # 直接加载图片 return 回去对比
        ic_image = Image.open(BytesIO(incomplete_img))
        # ic_image.show()
        # 保存图片(可不必保存)
        ic_image.save('ic_image.png')

        return c_image, ic_image

    def is_pixel_similar(self, c_image, ic_image, x, y):
        """
        比较两张图片的像素点

        注意: 像素点比较是有偏差的, 需要允许一定范围的误差,
            我们可以设置一个阈值
        :param ic_image:
        :param c_image:
        :param x:
        :param y:
        :return: 当像素点不相同时, 返回 False
        """
        # 获取两张图片执行位置的像素点
        c_pixel = c_image.load()[x, y]
        ic_pixel = ic_image.load()[x, y]
        # 阈值 允许误差
        threshold = 10
        # 对比
        if abs(c_pixel[0] - ic_pixel[0]) < threshold and \
                abs(c_pixel[1] - ic_pixel[1]) < threshold and \
                abs(c_pixel[2] - ic_pixel[2]) < threshold:
            return True
        return False

    def get_slice_gap(self, c_image, ic_image):
        """
        获取缺口的偏移量

        通过比较两张图片的所有像素点, 获取两张图片是从哪里开始不同
        从而得到 移动块 要在 x 方向移动的距离

        :param c_image: 完整的图片
        :param ic_image: 有缺失的图片
        :return: 缺口的偏移量
        """
        # ic_image.size:['width', 'height']
        for x in range(c_image.size[0]):
            for y in range(c_image.size[1]):
                if not self.is_pixel_similar(c_image, ic_image, x, y):
                    # 移动块只在水平方向移动 只需返回 x
                    return x

    def drag_slider(self, gap):
        """
        拖动滑块

        :param gap: 需要拖动的距离
        :return: None
        """
        slider = self.wait.until(
            EC.presence_of_element_located(
                (By.XPATH,
                 '/html/body/div[2]/div[2]/div[6]/div/div[1]/div[2]/div[2]')))
        # 抓住滑块
        ActionChains(self.browser).click_and_hold(on_element=slider).perform()
        # 移动 只在水平方向上移动
        ActionChains(self.browser).move_by_offset(xoffset=gap,
                                                  yoffset=0).perform()
        # 释放滑块
        ActionChains(self.browser).release().perform()

    def login_success(self):
        """
        判断是否登陆成功
        :return: 成功返回 True 失败返回False
        """
        try:
            # 登录成功后 界面上会有一个消息按钮
            return bool(
                WebDriverWait(self.browser, 5).until(
                    EC.presence_of_element_located(
                        (By.XPATH, '//a[@title="消息"]'))))
        except TimeoutException:
            return False

    def login(self):
        """
        开始

        :return: None
        """
        # 打开浏览器, 输入账号 密码, 点击登陆
        self.open()
        # 获取验证图 ic_image(有缺失的验证图) c_image(完整的验证图)
        c_image, ic_image = self.get_geetest_image()

        # 获取缺口的偏移量
        gap = self.get_slice_gap(c_image, ic_image)
        print(f'缺口的偏移量为:{gap}')
        # 拖动滑块
        # TODO 这边一直有一定的误差 暂时用测量工具解决
        self.drag_slider(gap - 8)
        time.sleep(3)

        if self.login_success():
            print('登陆成功')
        else:
            self.login()


if __name__ == '__main__':
    login = LoginBiliBili('*******', '******')
    login.login()